/**\file
 * functions for storing persisent configuration table.
 */

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <stdarg.h>
#include <assert.h>
#include <ctype.h>
#include <unistd.h>
#include <getopt.h>
#include <limits.h>

#include <sys/stat.h>
#include <sys/types.h>

#include "io_thread.h"
#include "cconf.h"
#include "validator.h"
#include "error.h"
#include "value.h"
#include "entry.h"
#include "table.h"
#include "config_type.h"

static int cconf_parse_line(char *line, char **name, char **value);

static int strcat_dynamic(char ** str, char * add)
{	
    if (*str == NULL)
		*str = calloc(1, sizeof(char));
    
    *str = realloc(*str, strlen(*str) + strlen(add) + sizeof(char));
    if (!*str)
        return -1;
    
    strcat(*str, add);
    return 0;
}


static int optlist_handler(int ch, const char * name, char * arg, void * data)
{
	cconf_t * c = data;
	int ret = 0;
	
	switch (ch)
	{
		case 'c':
			cconf_set_value(c, (char *) name, arg);
			break;
		case 'h':
			cconf_document_file(c, stdout, arg);	
			return 1;
		case 'f':
			if (0 != cconf_read(c, arg))
				fprintf(stderr, "failed to read configuration file %s\n", arg);
			break;
		default:
			ret = 1;
			break;	
	}
	
	return ret;
}

static char * _cconf_cmd(cconf_t * c, char * command)
{
	char * out;
	char * cmd = strdup(command), * arg = NULL;
	
	if ((arg = strchr(cmd, ' ')) != NULL)
		*(arg++) = '\0';
	
	if (!strcmp(cmd, "doc"))
	{
		out = cconf_document_entry(c, arg);
	}
	else if (!strcmp(cmd, "dump"))
	{
		out = cconf_dump(c, ";", "=");
	}
	else 	
		asprintf(&out, "bad command \"%s\"\n", command);
		
	free(cmd);
		
	return out;
}


static int message_callback(char ** out, char * in, void * arg)
{
	cconf_t * c = arg;
	char * name, * value;
	
	if (NULL == in)
		return 0;
	
	if (in[0] == '_')
	{
		*out = _cconf_cmd(c, in+1);
		return 0;
	}
	
	if (0 != cconf_parse_line(in, &name, &value))
	{
		*out = cconf_get_value_strc(c, "%s", in);
		
		if (*out == NULL)
			*out = cconf_get_errors(c);
		
		return 0;
	}
	
	cconf_set_value(c, name, value);
	
	if (cconf_check_errors(c))
		*out = cconf_get_errors(c);
	else 
	{
		asprintf(out, "%s", "gotcha");
		
		if (c->change_cb)
			c->change_cb(c, name, c->cb_arg);
	}
	
	free(name); free(value);

	return 0;
}

/**
 * create a configuration object.
 */

cconf_t * cconf_create(
	char * name, cconf_changed_fcn_t change_cb, void * cb_arg)
{
	cconf_t * cfg = calloc(1, sizeof(cconf_t));
	
    cfg->table = cconf_table_create(CCONF_TABLE_SIZE);
    cfg->name = strdup(name);
    cfg->error_entry = NULL;
    cfg->change_cb = change_cb;
    cfg->cb_arg = cb_arg;
    
    cfg->optlist = cconf_optlist_create(&optlist_handler, cfg); 
    
    cconf_optlist_append(cfg->optlist, "help", optional_argument, 'h');
    cconf_optlist_append(cfg->optlist, "file", required_argument, 'f');
     
    char * sock_root = getenv("CCONF_SOCKET_PATH");
	if (sock_root == NULL)
		sock_root = CCONF_SOCKET_PATH;
		
	mkdir(sock_root, S_IRWXU);
     
	return cfg;
}

int cconf_start_io_thread(cconf_t * c, char * name)
{
	if (c->io_thread)
		return -1;
	
	c->io_thread = cconf_io_thread_create(name, &message_callback, c);
	assert(c->io_thread != 0);
	
	return 0;
}

int cconf_check_errors(cconf_t * config)
{
	return config->errors != NULL;
}

/** 
 * free storage associated with a configuration object
 */

int cconf_free(cconf_t * cfg)
{
    if (cfg == NULL)
        return 0;
        
    if (cfg->io_thread)
		cconf_io_thread_finish(cfg->io_thread);
    
	cconf_table_free(cfg->table);
    
	free(cfg->name);
	free(cfg->error_entry);
	
	cconf_optlist_free(cfg->optlist);

	free(cfg);
    
    return 0;
}

static cconf_entry_t * cconf_get_entry(cconf_t * c, char * key)
{
	return (cconf_entry_t *) cconf_table_get(c->table, key);
}

static int _cconf_add_entry(cconf_t * c, cconf_entry_t * e)
{
	int result;
	char * errstr;
	cconf_error_t error = CCONF_ERROR_NO_ERROR;
	
	result = cconf_table_insert(c->table, e);
    
    if (result < 0)
        cconf_entry_free(e);
    
    if (result == TABLE_FULL)
        error = CCONF_ERROR_TABLE_FULL;
    else if (result == TABLE_ENTRY_EXISTS)
        error = CCONF_ERROR_ENTRY_EXISTS;
    else if (result < 0)
		error = CCONF_ERROR_GENERIC;
    
    if (error != CCONF_ERROR_NO_ERROR)
    {
        cconf_push_error_code(c, error, 0);
        asprintf(&errstr, "Couldn't add configuration entry '%s'", e->path);
        cconf_push_error(c, errstr, CCONF_STR_NODUP, -1);
    }
    
    return error;
}

static int lc_path_valid(char * path)
{
	char * p = path;
	int ix;
	
	for (; *p != '\0'; p++)
	{
		ix = p - path;
		
		if ((path[0] == '_') || !(isalnum(*p) || *p == '_') || (ix > CCONF_MAX_ENTRY_LEN))
			return 0;
	}
	
	return 1;
}

static void lc_cconf_set_env_value(cconf_t * c, char * path, char * env)
{
	char * env_name, *env_val;
	
	if (!env || !strcmp(env, ""))
		return;
		
	if (!strcmp(env, "*"))
	{
		asprintf(&env_name, "lc_%s_%s", c->name, path);
		char *p;
		for (p = env_name; *p != '\0'; p++) *p = toupper(*p);
	}
	else
		env_name = strdup(env);
		
	env_val = getenv(env_name);
	
	if (env_val && (cconf_set_value(c, path, env_val) != 0))
	{
		char * str;
		asprintf(&str, "WARNING: invalid value of environment variable %s", env_name);
		cconf_push_error(c, str, CCONF_STR_NODUP, 0);
	}
	
	free(env_name);
	
}

static int cconf_add_entry(
	cconf_t * c, char * path, char * def, char * doc, char * env, cconf_validator_t * val)
{
	if (!lc_path_valid(path))
	{ 
		char * err;
		asprintf(&err, "invalid entry name \"%s\"", path);
		cconf_push_error(c, err, CCONF_STR_NODUP, -1);
		
		cconf_validator_free(val);
		return CCONF_ERROR_INVALID;
	}
	
    int result = _cconf_add_entry(c, cconf_entry_create(path, val, doc, def, NULL));
    
    if (result != CCONF_ERROR_NO_ERROR)
		return result;
	
	lc_cconf_set_env_value(c, path, env);	
		
    /*
     * give us a long option
     */
    
    return cconf_optlist_append(c->optlist, path, required_argument, 'c');
}

int cconf_add_entry_specifier(cconf_t * c, char * path, char * spec, char * def)
{
	char fullpath[CCONF_MAX_ENTRY_LEN];
	cconf_entry_t * par, *e;
	int n;
	
	if ((par = cconf_get_entry(c, path))== NULL)
		return CCONF_ERROR_NO_SUCH_ENTRY;

	n = snprintf(fullpath, CCONF_MAX_ENTRY_LEN, 
		"%s%c%s", spec, CCONF_NAMESPACE_SEP, path);
	
	if (n > CCONF_MAX_ENTRY_LEN)
		return CCONF_ERROR_GENERIC;
		
	e = cconf_entry_create(fullpath, NULL, NULL, def, par);
	cconf_entry_set_value(e, strdup(cconf_entry_get_value(par)));
	
	return _cconf_add_entry(c, e);
}

int cconf_add_entry_usr(
	cconf_t * c, char * path, char * def, char * docstring, char * env,
	int (*validate_fcn)(char **, char *, void *),
	void * arg, char * v_docstring
)
{
	cconf_validator_t * v = cconf_validator_new_user(validate_fcn, arg, v_docstring);
	return cconf_add_entry(c, path, def, docstring, env, v);
}

int cconf_add_entry_intrange(
	cconf_t * c, char * path, char * def, char * doc, char * env, int lo, int hi)
{
    cconf_validator_t * v = cconf_validator_new_intrange(lo, hi);
    return cconf_add_entry(c, path, def, doc, env, v);
}

int cconf_add_entry_doublerange(
	cconf_t * c, char * path, char * def, char * doc, char *env, double lo, double hi)
{
    cconf_validator_t * v = cconf_validator_new_doublerange(lo, hi);
    return cconf_add_entry(c, path, def, doc,env, v);
}

int cconf_add_entry_int(cconf_t * c, char * path, char * def, char * doc, char * env)
{
    cconf_validator_t * v = cconf_validator_new_int();
    return cconf_add_entry(c, path, def, doc, env, v);
}

int cconf_add_entry_double(cconf_t * c, char * path, char * def, char * doc, char * env)
{
    cconf_validator_t * v = cconf_validator_new_double();
    return cconf_add_entry(c, path, def, doc, env, v);
}

int cconf_add_entry_strlist(
	cconf_t * c, char * path, char * def, char * doc, char *env, char * list)
{
    cconf_validator_t * v = cconf_validator_new_strlist(list);
    return cconf_add_entry(c, path, def, doc, env, v);
}

int cconf_add_entry_str(cconf_t * c, char * path, char * def, char * doc, char * env)
{
    cconf_validator_t * v = cconf_validator_new_string();
    return cconf_add_entry(c, path, def, doc, env, v);
}

int cconf_add_entries(cconf_t * c, ...)
{
	char *arg, *name = NULL, *type, *dfl, *doc, *env;
    va_list arglist;
    va_start(arglist, c);
    
    while ((arg = va_arg(arglist, char *)) != NULL)
    {
		if (!name) { name = arg; dfl = NULL; continue; }	
		if (!dfl) { dfl = arg; doc = NULL; continue; }
		if (!doc) { doc = arg; env = NULL; continue; }
		if (!env) { env = arg; type = NULL; continue; }
		
		if (!type) 
		{
			type = arg;
			
			if (!strcmp(type, "int"))
			{
				cconf_add_entry_int(c, name, dfl, doc, env); name = NULL;
			}
			else if (!strcmp(type, "double"))
			{
				cconf_add_entry_double(c, name, dfl, doc, env); name = NULL;
			}
			else if (!strcmp(type, "str"))
			{
				cconf_add_entry_str(c, name, dfl, doc, env); name = NULL;
			}

			continue;
		}
				
		if (!strcmp(type, "strlist"))
			cconf_add_entry_strlist(c, name, dfl, doc, env, arg);
			
		else if (!strcmp(type, "doublerange"))
		{
			float lo, hi; sscanf(arg, "%f-%f", &lo, &hi);
			cconf_add_entry_doublerange(c, name, dfl, doc, env, lo, hi);
		}
		else if (!strcmp(type, "intrange"))
		{
			int lo, hi; sscanf(arg, "%d-%d", &lo, &hi);
			cconf_add_entry_intrange(c, name, dfl, doc, env, lo, hi);
		}		
		else
		{
			char *err;
			asprintf(&err, "couldn't create config entry of type %s\n", type);
			cconf_push_error(c, err, CCONF_STR_NODUP, 0);
		}
		name = NULL;
	}
    
    va_end(arglist);
    return 0;
}

static int cconf_entry_validate(cconf_t * c, cconf_entry_t * ent, char *value)
{
	cconf_entry_t * e = ent;
	char * val_error = NULL;
	int result;
	
	while (e->parent != NULL)
		e = e->parent;
			
	result = cconf_validator_validate(e->validator, &val_error, value);	
	
	if (result != CCONF_VALIDATE_ERROR_NO_ERROR)
	{
		cconf_push_error(c, val_error, CCONF_STR_NODUP, 0);	
		return cconf_push_error_code(c, CCONF_ERROR_INVALID, -1);
	}
	
	return CCONF_ERROR_NO_ERROR;
}


int cconf_set_value(cconf_t * c, char * path, char * value)
{
    cconf_error_t err = CCONF_ERROR_NO_ERROR;
    cconf_entry_t * e = cconf_get_entry(c, path);
    char * errstr;
    
    if (e == NULL)
        err = cconf_push_error_code(c, CCONF_ERROR_NO_SUCH_ENTRY, 0);
    else
    {        
		err = cconf_entry_validate(c, e, value);
    }
    
    if (err != CCONF_ERROR_NO_ERROR)
    {
        asprintf(&errstr, "Couldn't set value of '%s'", path);
        cconf_push_error(c, errstr, CCONF_STR_NODUP, -1);
        return err;
    }
    
    cconf_entry_set_value(e, strdup(value));
    
    return CCONF_ERROR_NO_ERROR;
}

/**
 * allow a specific key, represented by "specifier:" to override the default
 * configuration value.  We use a va_list so you can specify the key just as 
 * you would printf arguments.  i.e. cconf_get_value("%s:%s", specifier, base_key)
 */

static cconf_entry_t * cconf_get_value_va(cconf_t * c, char * format, va_list arglist)
{
    cconf_entry_t * entry = NULL;
    
    char key[CCONF_MAX_ENTRY_LEN], * key_ptr = key;
    int N;
     
	N = vsnprintf(key, CCONF_MAX_ENTRY_LEN - 1, format, arglist);
    
    if (N >= CCONF_MAX_ENTRY_LEN){
		fprintf(stderr, "configuration entry name too long!\n");
	}
    
    /*
     * now get the configuration value
     */
    
    while (key_ptr != NULL)
	{
		entry = cconf_get_entry(c, key_ptr);
		
		if (entry != NULL)
			break;
        
        key_ptr = strchr(key_ptr, CCONF_NAMESPACE_SEP);
		
		if (key_ptr != NULL)
			key_ptr++;
	}
	
	if (entry == NULL)
		cconf_push_error_code(c, CCONF_ERROR_NO_SUCH_ENTRY, 0);

	return entry;
}

/**
 * generate the code to call a function from a variable argument list
 */

#define VA_HELPER(c, out, in, fcn) \
va_list arglist; \
va_start(arglist, in); \
out = fcn(c, in, arglist); \
va_end(arglist);

/**
 * get a string value, with optional specifier.
 */

char * cconf_get_value_str(cconf_t * c, char * format, ...)
{
    cconf_entry_t * e;
    VA_HELPER(c, e, format, cconf_get_value_va)
    
    if (e == NULL)
        return NULL;
    
    return cconf_entry_get_value(e);
}

char * cconf_get_value_strc(cconf_t * c, char * format, ...)
{
    cconf_entry_t * e;
    VA_HELPER(c, e, format, cconf_get_value_va)
    
    if (e == NULL)
        return NULL;
    
    return strdup(cconf_entry_get_value(e));
}

char ** cconf_get_value_strlist(cconf_t * c, char * format, ...)
{
	cconf_entry_t * e;
    VA_HELPER(c, e, format, cconf_get_value_va)
    
    if (e == NULL)
        return NULL;
    
    return str_to_list(cconf_entry_get_value(e), ",");
}

/**
 * get an integer value from the configuration
 * NOTE: this should perform a parse-error check.
 */

int cconf_get_value_int(cconf_t * c, char * format, ...)
{
	cconf_entry_t * e;
    VA_HELPER(c, e, format, cconf_get_value_va)
    
    if (e == NULL)
        return 0;
		
	return atoi(cconf_entry_get_value(e));
}

/**
 * get a float value.
 * NOTE: this should perform a parse-error check.
 */

float cconf_get_value_float(cconf_t * c, char * format, ...)
{
	cconf_entry_t * e;
    VA_HELPER(c, e, format, cconf_get_value_va)
    
    if (e == NULL)
        return 0;
		
	return atof(cconf_entry_get_value(e));
}


static int cconf_parse_line(char *line, char **name, char **value)
{		
	int result = sscanf(line, " %a[a-zA-Z0-9_:]%*[ \t]%a[^\n]", name, value);
	
	fprintf(stderr, "config line %s parses as %d items\n", line, result);
	
	switch (result)
	{
		case 2: return 0;
		case 1: free(*name);
		case 0:
		default:
			return 1;
	}
}

/**
 * read a config file into the config object
 */

int cconf_read(cconf_t * c, char * config_file)
{
	FILE * fp;
	char line[LINE_MAX], *name, *value, *err;
	int ln = 0, length;
	
	fp = fopen(config_file, "r");
	
	if (!fp)
		return CCONF_ERROR_BAD_FILE; 
	
	for (ln = 0; fgets(line, LINE_MAX, fp) != NULL; ln++)
	{  
		length = strlen(line);
		
		if ((line[0] == '#') || (length < 3))
			continue;
			        
        if (cconf_parse_line(line, &name, &value))
		{
			asprintf(&err, "bad configuration line %d", ln);
			cconf_push_error(c, err, CCONF_STR_NODUP, 1);
			continue;
		}
        
		if (cconf_set_value(c, name, value) != 0)
		{
			asprintf(&err, "bad configuration line %d", ln);
			cconf_push_error(c, err, CCONF_STR_NODUP, 1);
		}
		
		free(name); free(value);
	}
	
	fclose(fp);
	return 0;
}

/**
 * return a single-string dump of a configuration object's contents
 */

char * cconf_dump(cconf_t *c, char * rs, char * dl)
{
    char * output = NULL;
    int i, n;
    char ** names;
    cconf_entry_t ** entries;
    
    n = cconf_table_get_all(c->table, &names, (void ***) &entries);
    
    output = strdup("");
    
    for (i = 0; i < n; i++)
    {
		strcat_dynamic(&output, names[i]);
		strcat_dynamic(&output, dl);
		strcat_dynamic(&output, cconf_entry_get_value(entries[i]));
		strcat_dynamic(&output, rs);
    }
    
    free(names); 
    free(entries);
    
	return output;
}

void cconf_print(cconf_t * c)
{
    char * dump = cconf_dump(c, ";",",");

    printf("%s\n", dump);
    free(dump);
}

char * cconf_document_entry(cconf_t * c, char * entry_name)
{
	if (!entry_name)
		return cconf_document(c);
		
	cconf_entry_t * e = cconf_table_get(c->table, entry_name);
	return cconf_entry_document(e);
}

char * cconf_document(cconf_t * c)
{
    char * output;
    char * line;
    int i, n;
    char ** names;
    cconf_entry_t ** entries;
    
    n = cconf_table_get_all(c->table, &names, (void ***) &entries);
    
    output = strdup("");

    for (i = 0; i < n; i++)
    {
		line = cconf_document_entry(c, names[i]);
        
        strcat_dynamic(&output, line);
        strcat_dynamic(&output, "\n");
        
        free(line);
    }
    
    free(names);
    free(entries);
 
    return output;
}

int cconf_document_file(cconf_t * cfg, FILE * fp, char * name)
{
	char * dump = cconf_document_entry(cfg, name);
	fprintf(fp, "%s\n", dump);
	free(dump);
	return 0;
}

int cconf_optparse(cconf_t * c, int argc, char ** argv)
{
    return cconf_optlist_parse(c->optlist, argc, argv);
}

char * cconf_get_errors(cconf_t * c)
{
    char *out = NULL;
    cconf_error_stack_t * err;
        
    err = cconf_pop_error(c);
    
    while (err != NULL)
    {
        strcat_dynamic(&out, err->error);
        strcat_dynamic(&out, err->level ? ": " : "\n");
        
        free(err->error);
        free(err);
        
        err = cconf_pop_error(c);
    }
    
    return out;
}

/**
 * return true if the configuration contains an entry with a specified key
 */

int cconf_has_entry(cconf_t * c, char * config_entry_name)
{
	char * value = cconf_get_value_str(c, config_entry_name);

    return !(value == NULL);
}
